#!/usr/bin/env python3
"""
Docker端口配置审计工具
==================

审计RedFire后端项目中Docker配置的端口映射问题
- 检查Dockerfile中的EXPOSE端口与实际运行端口是否一致
- 检查docker-compose.yml中的端口映射
- 检查健康检查的端口配置
- 生成端口配置修复建议

执行: python tools/docker_port_audit.py
"""

import os
import re
import json
import yaml
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, asdict

@dataclass
class ServicePortInfo:
    """服务端口信息"""
    service_name: str
    dockerfile_expose_port: Optional[int] = None
    compose_external_port: Optional[int] = None
    compose_internal_port: Optional[int] = None
    healthcheck_port: Optional[int] = None
    runtime_port: Optional[int] = None
    config_port: Optional[int] = None
    errors: List[str] = None
    
    def __post_init__(self):
        if self.errors is None:
            self.errors = []

@dataclass
class PortAuditResult:
    """端口审计结果"""
    total_services: int
    services_with_errors: int
    port_conflicts: List[Dict[str, Any]]
    port_assignments: Dict[str, int]
    service_details: List[ServicePortInfo]
    recommendations: List[str]


class DockerPortAuditor:
    """Docker端口配置审计器"""
    
    def __init__(self, backend_path: str = "backend"):
        self.backend_path = Path(backend_path)
        self.apps_path = self.backend_path / "apps"
        self.deploy_path = self.backend_path / "deploy"
        self.results = {}
        
        # 标准端口分配
        self.standard_ports = {
            "api-gateway": 8000,
            "user_service": 8001, 
            "auth-service": 8002,
            "trading-service": 8003,
            "strategy-service": 8004,
            "market-data-service": 8005,
            "notification-service": 8006,
            "monitoring-service": 8007
        }
        
    def audit_all_services(self) -> PortAuditResult:
        """审计所有服务的端口配置"""
        print("🔍 开始Docker端口配置审计...")
        
        service_infos = []
        port_conflicts = []
        
        # 获取所有服务
        services = self._get_all_services()
        
        for service_name in services:
            print(f"  📋 审计服务: {service_name}")
            service_info = self._audit_service(service_name)
            service_infos.append(service_info)
            
        # 检查端口冲突
        port_conflicts = self._check_port_conflicts(service_infos)
        
        # 生成建议
        recommendations = self._generate_recommendations(service_infos, port_conflicts)
        
        # 统计错误
        services_with_errors = sum(1 for info in service_infos if info.errors)
        
        result = PortAuditResult(
            total_services=len(service_infos),
            services_with_errors=services_with_errors,
            port_conflicts=port_conflicts,
            port_assignments=self.standard_ports.copy(),
            service_details=service_infos,
            recommendations=recommendations
        )
        
        print(f"✅ 审计完成: {len(service_infos)}个服务, {services_with_errors}个有错误")
        return result
    
    def _get_all_services(self) -> List[str]:
        """获取所有服务名称"""
        services = []
        if self.apps_path.exists():
            for item in self.apps_path.iterdir():
                if item.is_dir() and (item / "Dockerfile").exists():
                    services.append(item.name)
        return sorted(services)
    
    def _audit_service(self, service_name: str) -> ServicePortInfo:
        """审计单个服务的端口配置"""
        info = ServicePortInfo(service_name=service_name)
        
        # 检查Dockerfile
        self._check_dockerfile_port(service_name, info)
        
        # 检查docker-compose.yml
        self._check_compose_port(service_name, info)
        
        # 检查运行时端口配置
        self._check_runtime_port(service_name, info)
        
        # 检查配置文件端口
        self._check_config_port(service_name, info)
        
        # 验证端口一致性
        self._validate_port_consistency(info)
        
        return info
    
    def _check_dockerfile_port(self, service_name: str, info: ServicePortInfo):
        """检查Dockerfile中的EXPOSE端口"""
        dockerfile_path = self.apps_path / service_name / "Dockerfile"
        
        if not dockerfile_path.exists():
            info.errors.append(f"Dockerfile不存在: {dockerfile_path}")
            return
            
        try:
            content = dockerfile_path.read_text(encoding='utf-8')
            
            # 查找EXPOSE指令
            expose_pattern = r'EXPOSE\s+(\d+)'
            matches = re.findall(expose_pattern, content)
            
            if matches:
                info.dockerfile_expose_port = int(matches[0])
                if len(matches) > 1:
                    info.errors.append(f"Dockerfile中有多个EXPOSE端口: {matches}")
            else:
                info.errors.append("Dockerfile中未找到EXPOSE指令")
                
        except Exception as e:
            info.errors.append(f"读取Dockerfile失败: {e}")
    
    def _check_compose_port(self, service_name: str, info: ServicePortInfo):
        """检查docker-compose.yml中的端口映射"""
        compose_path = self.deploy_path / "docker-compose.yml"
        
        if not compose_path.exists():
            info.errors.append(f"docker-compose.yml不存在: {compose_path}")
            return
            
        try:
            with open(compose_path, 'r', encoding='utf-8') as f:
                compose_data = yaml.safe_load(f)
            
            services = compose_data.get('services', {})
            service_data = services.get(service_name, {})
            
            if not service_data:
                info.errors.append(f"在docker-compose.yml中未找到服务: {service_name}")
                return
            
            # 检查端口映射
            ports = service_data.get('ports', [])
            if ports:
                port_mapping = ports[0]  # 取第一个端口映射
                if ':' in port_mapping:
                    external, internal = port_mapping.split(':')
                    info.compose_external_port = int(external)
                    info.compose_internal_port = int(internal)
                else:
                    info.compose_internal_port = int(port_mapping)
                    info.compose_external_port = int(port_mapping)
            
            # 检查健康检查端口
            healthcheck = service_data.get('healthcheck', {})
            if healthcheck:
                test_cmd = healthcheck.get('test', [])
                if test_cmd and len(test_cmd) > 0:
                    # 查找健康检查中的端口
                    health_cmd = ' '.join(test_cmd)
                    port_pattern = r'localhost:(\d+)'
                    port_match = re.search(port_pattern, health_cmd)
                    if port_match:
                        info.healthcheck_port = int(port_match.group(1))
                        
        except Exception as e:
            info.errors.append(f"读取docker-compose.yml失败: {e}")
    
    def _check_runtime_port(self, service_name: str, info: ServicePortInfo):
        """检查代码中的运行时端口配置"""
        main_py_path = self.apps_path / service_name / "app" / "main.py"
        
        if not main_py_path.exists():
            return
            
        try:
            content = main_py_path.read_text(encoding='utf-8')
            
            # 查找uvicorn.run中的port参数
            port_patterns = [
                r'uvicorn\.run\([^)]*port=(\d+)',
                r'port=(\d+)',
                r'--port[=\s]+(\d+)'
            ]
            
            for pattern in port_patterns:
                matches = re.findall(pattern, content)
                if matches:
                    info.runtime_port = int(matches[0])
                    break
                    
        except Exception as e:
            info.errors.append(f"读取运行时配置失败: {e}")
    
    def _check_config_port(self, service_name: str, info: ServicePortInfo):
        """检查配置文件中的端口设置"""
        # 检查统一配置中的端口
        standard_port = self.standard_ports.get(service_name)
        if standard_port:
            info.config_port = standard_port
    
    def _validate_port_consistency(self, info: ServicePortInfo):
        """验证端口配置一致性"""
        ports = []
        
        if info.dockerfile_expose_port:
            ports.append(("Dockerfile EXPOSE", info.dockerfile_expose_port))
        if info.compose_internal_port:
            ports.append(("Compose内部端口", info.compose_internal_port))
        if info.healthcheck_port:
            ports.append(("健康检查端口", info.healthcheck_port))
        if info.runtime_port:
            ports.append(("运行时端口", info.runtime_port))
        if info.config_port:
            ports.append(("配置端口", info.config_port))
        
        # 检查是否一致
        if len(ports) > 1:
            port_values = [port[1] for port in ports]
            if len(set(port_values)) > 1:
                port_details = ", ".join([f"{name}: {port}" for name, port in ports])
                info.errors.append(f"端口配置不一致: {port_details}")
    
    def _check_port_conflicts(self, service_infos: List[ServicePortInfo]) -> List[Dict[str, Any]]:
        """检查端口冲突"""
        conflicts = []
        port_usage = {}
        
        for info in service_infos:
            if info.compose_external_port:
                port = info.compose_external_port
                if port in port_usage:
                    conflicts.append({
                        "port": port,
                        "services": [port_usage[port], info.service_name],
                        "type": "external_port_conflict"
                    })
                else:
                    port_usage[port] = info.service_name
        
        return conflicts
    
    def _generate_recommendations(self, service_infos: List[ServicePortInfo], 
                                conflicts: List[Dict[str, Any]]) -> List[str]:
        """生成修复建议"""
        recommendations = []
        
        # 端口冲突建议
        if conflicts:
            recommendations.append("🔴 高优先级: 修复端口冲突")
            for conflict in conflicts:
                recommendations.append(
                    f"  - 端口 {conflict['port']} 被多个服务使用: {', '.join(conflict['services'])}"
                )
        
        # 端口不一致建议
        inconsistent_services = [info for info in service_infos if 
                                any("端口配置不一致" in error for error in info.errors)]
        if inconsistent_services:
            recommendations.append("🟠 中优先级: 统一端口配置")
            for info in inconsistent_services:
                standard_port = self.standard_ports.get(info.service_name, "未定义")
                recommendations.append(
                    f"  - {info.service_name}: 建议统一使用端口 {standard_port}"
                )
        
        # 缺失配置建议
        missing_expose = [info for info in service_infos if not info.dockerfile_expose_port]
        if missing_expose:
            recommendations.append("🟡 中优先级: 添加缺失的EXPOSE指令")
            for info in missing_expose:
                recommendations.append(f"  - {info.service_name}: 添加EXPOSE指令")
        
        # 健康检查建议
        missing_health = [info for info in service_infos if not info.healthcheck_port]
        if missing_health:
            recommendations.append("🟢 低优先级: 完善健康检查配置")
            for info in missing_health:
                recommendations.append(f"  - {info.service_name}: 配置健康检查端口")
        
        return recommendations


def generate_audit_report(result: PortAuditResult, output_dir: str = "backend/reports"):
    """生成审计报告"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    # 确保输出目录存在
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)
    
    # 生成详细报告
    report_file = output_path / f"docker_port_audit_detailed_{timestamp}.md"
    summary_file = output_path / f"docker_port_audit_summary_{timestamp}.md"
    data_file = output_path / f"docker_port_audit_data_{timestamp}.json"
    
    # 生成详细Markdown报告
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(f"""# RedFire Docker端口配置审计详细报告

**生成时间**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
**审计工具**: tools/docker_port_audit.py

## 🎯 审计总览

- **总服务数**: {result.total_services}
- **有错误的服务数**: {result.services_with_errors}
- **端口冲突数**: {len(result.port_conflicts)}
- **标准端口分配**: {len(result.port_assignments)}

## 📊 端口分配标准

| 服务名称 | 标准端口 | 状态 |
|---------|----------|------|
""")
        
        for service, port in result.port_assignments.items():
            status = "✅ 正确" if any(
                info.service_name == service and not info.errors 
                for info in result.service_details
            ) else "❌ 有问题"
            f.write(f"| {service} | {port} | {status} |\n")
        
        # 端口冲突详情
        if result.port_conflicts:
            f.write(f"\n## 🔴 端口冲突详情\n\n")
            for i, conflict in enumerate(result.port_conflicts, 1):
                f.write(f"### 冲突 {i}: 端口 {conflict['port']}\n")
                f.write(f"**冲突服务**: {', '.join(conflict['services'])}\n")
                f.write(f"**冲突类型**: {conflict['type']}\n\n")
        
        # 服务详情
        f.write(f"\n## 📋 服务配置详情\n\n")
        for info in result.service_details:
            f.write(f"### {info.service_name}\n\n")
            f.write(f"| 配置项 | 端口 | 状态 |\n")
            f.write(f"|--------|------|------|\n")
            f.write(f"| Dockerfile EXPOSE | {info.dockerfile_expose_port or 'N/A'} | {'✅' if info.dockerfile_expose_port else '❌'} |\n")
            f.write(f"| Compose外部端口 | {info.compose_external_port or 'N/A'} | {'✅' if info.compose_external_port else '❌'} |\n")
            f.write(f"| Compose内部端口 | {info.compose_internal_port or 'N/A'} | {'✅' if info.compose_internal_port else '❌'} |\n")
            f.write(f"| 健康检查端口 | {info.healthcheck_port or 'N/A'} | {'✅' if info.healthcheck_port else '❌'} |\n")
            f.write(f"| 运行时端口 | {info.runtime_port or 'N/A'} | {'✅' if info.runtime_port else '❌'} |\n")
            f.write(f"| 标准配置端口 | {info.config_port or 'N/A'} | {'✅' if info.config_port else '❌'} |\n")
            
            if info.errors:
                f.write(f"\n**⚠️ 发现的问题**:\n")
                for error in info.errors:
                    f.write(f"- {error}\n")
            f.write(f"\n")
        
        # 修复建议
        if result.recommendations:
            f.write(f"\n## 🔧 修复建议\n\n")
            for recommendation in result.recommendations:
                f.write(f"{recommendation}\n")
        
        f.write(f"\n---\n\n*此报告由Docker端口审计工具自动生成*")
    
    # 生成总结报告
    with open(summary_file, 'w', encoding='utf-8') as f:
        f.write(f"""# RedFire Docker端口配置审计总结

**审计时间**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

## 🎯 关键发现

- **审计服务数**: {result.total_services}个
- **问题服务数**: {result.services_with_errors}个
- **端口冲突**: {len(result.port_conflicts)}个
- **问题比率**: {(result.services_with_errors/result.total_services)*100:.1f}%

## 🔴 急需修复的问题

""")
        
        critical_issues = [rec for rec in result.recommendations if "🔴" in rec]
        if critical_issues:
            for issue in critical_issues:
                f.write(f"{issue}\n")
        else:
            f.write("✅ 无严重问题\n")
        
        f.write(f"\n## 📈 下一步行动\n\n")
        f.write(f"1. 修复端口冲突\n")
        f.write(f"2. 统一端口配置\n")
        f.write(f"3. 完善健康检查\n")
        f.write(f"4. 标准化Docker配置\n")
        
        f.write(f"\n详细报告: `{report_file.name}`\n")
        f.write(f"原始数据: `{data_file.name}`\n")
    
    # 保存原始数据
    with open(data_file, 'w', encoding='utf-8') as f:
        # 转换dataclass为字典
        data = {
            'audit_info': {
                'timestamp': timestamp,
                'total_services': result.total_services,
                'services_with_errors': result.services_with_errors,
                'port_conflicts_count': len(result.port_conflicts)
            },
            'port_assignments': result.port_assignments,
            'port_conflicts': result.port_conflicts,
            'service_details': [asdict(info) for info in result.service_details],
            'recommendations': result.recommendations
        }
        json.dump(data, f, ensure_ascii=False, indent=2)
    
    print(f"📄 生成报告:")
    print(f"  - 详细报告: {report_file}")
    print(f"  - 总结报告: {summary_file}")
    print(f"  - 原始数据: {data_file}")
    
    return report_file, summary_file, data_file


def main():
    """主函数"""
    print("🚀 RedFire Docker端口配置审计工具")
    print("=" * 50)
    
    # 检查工作目录
    if not Path("backend").exists():
        print("❌ 错误: 请在项目根目录运行此脚本")
        return
    
    try:
        # 创建审计器
        auditor = DockerPortAuditor()
        
        # 执行审计
        result = auditor.audit_all_services()
        
        # 生成报告
        print("\n📝 生成审计报告...")
        report_files = generate_audit_report(result)
        
        # 显示总结
        print(f"\n✅ 审计完成!")
        print(f"🎯 总服务数: {result.total_services}")
        print(f"❌ 问题服务: {result.services_with_errors}")
        print(f"⚠️  端口冲突: {len(result.port_conflicts)}")
        
        if result.recommendations:
            print(f"\n💡 关键建议:")
            for i, rec in enumerate(result.recommendations[:3], 1):
                print(f"  {i}. {rec}")
        
    except Exception as e:
        print(f"❌ 审计失败: {e}")
        raise


if __name__ == "__main__":
    main()
